﻿using System;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Diagnostics;

namespace Batch_Disc_Backup
{
    /// <summary>
    /// Process that will copy a disc to the 'to' location, eject the disc tray, wait for the disc, and repeat.
    /// </summary>
    public class DiscCopier
    {
        //To make logging easy, all loggable information is in this DiscCopyInfo object
        private DiscCopyInfo discCopyInfo = null;
        public DiscCopyInfo DiscCopyInfo { get { return discCopyInfo; } }
        private string rootFrom;
        public string RootFrom { get { return rootFrom; } }
        private string rootTo;
        public string RootTo { get { return rootTo; } }
        private string driveLetter;
        public string DriveLetter { get { return driveLetter; } }
        private int lastErrorCount = 0;
        public int LastErrorCount { get { return lastErrorCount; } set { lastErrorCount = value; } }
        
        private bool stop;

        private LinkedList<CopySnapshot> copySnapshots = new LinkedList<CopySnapshot>();
        public double TotalBytesPerSecond
        {
            get
            {
                if (copySnapshots.Count == 0) return 0;
                double seconds = (double)(copySnapshots.Last.Value.Time - copySnapshots.First.Value.Time) / 10000000;
                if (seconds == 0) return 0;
                double bytesCopiedDif = copySnapshots.Last.Value.BytesCopied - copySnapshots.First.Value.BytesCopied;
                return bytesCopiedDif / seconds;
            }
        }
        private int currentFileNo = 0;
        public long CurrentFileNo { get { return currentFileNo; } }
        private long currentBytesCopied = 0;
        public long CurrentBytesCopied { get { return currentBytesCopied; } }
        private long currentFileLength = 0;
        public long CurrentFileLength { get { return currentFileLength; } }
        private string status = null;
        public string Status { get { return status; } }
        private StatusType statusType = StatusType.Preparing;
        public StatusType StatusType { get { return statusType; } }
        public delegate void UpdateDelegate(DiscCopier process, Exception ex);
        public delegate void EndedDelegate(DiscCopier process);
        public event UpdateDelegate OnUpdate;
        public event EndedDelegate OnEnded;
        private bool isProcessing = false;
        public bool IsProcessing { get { return isProcessing; } }


        public DiscCopier(string driveLetter, string rootTo, UpdateDelegate onUpdate, EndedDelegate onEnded)
        {
            this.rootFrom = driveLetter + @":\";
            this.rootTo = rootTo;
            this.driveLetter = driveLetter;
            this.stop = false;
            this.status = "Preparing...";
            this.OnUpdate += onUpdate;
            this.OnEnded += onEnded;
        }

        public void Start()
        {
            new Thread(StartThread).Start();
        }

        public void Stop()
        {
            stop = true;
        }

        
        private DriveInfo lastDriveInfo = null;
        /// <summary>
        /// The main thread
        /// </summary>
        private void StartThread()
        {
            while (!stop)
            {
                discCopyInfo = new DiscCopyInfo(driveLetter, rootFrom, rootTo);
                UpdateStatus(StatusType.Preparing, null);

                //Continue to attempt to get the disc's file contents
                //If it cannot be read, we assume that there is no
                //disc in the drive yet.
                while (!stop)
                {
                    try
                    {
                        DriveInfo driveInfo = new DriveInfo(driveLetter);
                        if (!driveInfo.IsReady)
                        {
                            lastDriveInfo = null; //We at least know now that the drive has been opened
                            throw new Exception("Drive not ready yet");
                        }
                        //The previous loop may have failed while trying to eject the disc.
                        //In which case we check this so that we don't copy the same disc repeatedly
                        else if (lastDriveInfo != null
                            && lastDriveInfo.AvailableFreeSpace == driveInfo.AvailableFreeSpace
                            && lastDriveInfo.TotalSize == driveInfo.TotalSize
                            && lastDriveInfo.VolumeLabel == driveInfo.VolumeLabel)
                        {
                            throw new Exception("Disc has still not yet ejected");
                        }
                        string to = Quick.NextDir(Path.Combine(rootTo, driveInfo.VolumeLabel));
                        discCopyInfo.VolumeLabel = driveInfo.VolumeLabel;
                        discCopyInfo.ToDir = to;
                        AddFilesToCopyFiles(rootFrom, to);
                        lastDriveInfo = driveInfo;
                        break;
                    }
                    catch (Exception ex)
                    {
                        if (stop) break;

                        UpdateStatus(StatusType.WaitingForDisc, null);
                        Thread.Sleep(1000);
                    }
                }
                if (stop) break;

                byte[] buffer = new byte[512 * 1024]; //half a KB buffer

                isProcessing = true;
                discCopyInfo.DateStarted = DateTime.Now;
                Log.Write(discCopyInfo);
                for (int x = 0; x < discCopyInfo.FilesToCopy.Count; x++)
                {
                    if (stop) break;

                    FileToCopy fileToCopy = discCopyInfo.FilesToCopy[x];
                    currentFileLength = 0; currentBytesCopied = 0;

                    try
                    {
                        using (FileStream fsFrom = new FileStream(fileToCopy.FormFile, FileMode.Open, FileAccess.Read))
                        {
                            currentFileLength = fsFrom.Length;
                            UpdateStatus(StatusType.Copying, Path.GetFileName(fileToCopy.FormFile) + " (" + Path.GetDirectoryName(fileToCopy.FormFile) + ")");

                            using (FileStream fsTo = new FileStream(fileToCopy.ToFile, FileMode.CreateNew, FileAccess.Write))
                            {
                                int bytesRead;
                                long lastUpdateTime = -1;
                                long thisUpdateTime;
                                while (!stop && (bytesRead = fsFrom.Read(buffer, 0, buffer.Length)) > 0)
                                {
                                    fsTo.Write(buffer, 0, bytesRead);

                                    currentBytesCopied += bytesRead;
                                    discCopyInfo.TotalBytesCopied += bytesRead;

                                    //Notify the UI of the progress every 1/2 of a second
                                    thisUpdateTime = DateTime.Now.Ticks;
                                    if (lastUpdateTime == -1 || thisUpdateTime - lastUpdateTime > 5000000)
                                    {
                                        //Log to track the bytes persecond
                                        copySnapshots.AddLast(new CopySnapshot(discCopyInfo.TotalBytesCopied, thisUpdateTime));
                                        while (copySnapshots.Count > 10) copySnapshots.RemoveFirst();
                                    }
                                    //If it's the first loop
                                    if (lastUpdateTime == -1) lastUpdateTime = thisUpdateTime;
                                    //Send an update every half second
                                    if (thisUpdateTime - lastUpdateTime > 5000000)
                                    {
                                        OnUpdate(this, null);
                                        lastUpdateTime = thisUpdateTime;
                                    }
                                }
                                if (!stop) fileToCopy.CopyComplete = true;
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        if (fileToCopy.ExceptionMessage == null) discCopyInfo.ExceptionCount++;
                        fileToCopy.ExceptionMessage = ex.Message;
                        lastErrorCount++;
                        UpdateStatus(StatusType.Error, ex.Message, ex:ex);

                        //If disc is not ready, then it has been ejected. Cancel from here
                        if (!new DriveInfo(driveLetter).IsReady) break;
                    }
                    finally
                    {
                        //Delete the half copied file if required
                        if (!fileToCopy.CopyComplete)
                            File.Delete(fileToCopy.ToFile);
                    }
                    currentFileNo++;
                }

                discCopyInfo.DateEnded = DateTime.Now;
                Log.Write(discCopyInfo);
                isProcessing = false;
                discCopyInfo = null;
                copySnapshots.Clear();

                UpdateStatus(StatusType.WaitingForDisc, null);
                if (!stop)
                {
                    OpenTray(driveLetter);
                }
            }

            UpdateStatus(StatusType.Done, null, true);
        }

        /// <summary>
        /// Grab all of the files from the disc directory
        /// </summary>
        /// <param name="fromDir"></param>
        /// <param name="toDir"></param>
        private void AddFilesToCopyFiles(string fromDir, string toDir)
        {
            if (!Directory.Exists(toDir))
                Directory.CreateDirectory(toDir);

            //Copy sub dirs
            string[] dirs = Directory.GetDirectories(fromDir);
            foreach (string dir in dirs)
            {
                AddFilesToCopyFiles(dir, Path.Combine(toDir, Path.GetFileName(dir)));
            }

            //Copy files
            string[] files = Directory.GetFiles(fromDir);
            foreach (string file in files)
            {
                discCopyInfo.FilesToCopy.Add(new FileToCopy(file, Path.Combine(toDir, Path.GetFileName(file))));
                discCopyInfo.TotalFilesLength += new FileInfo(file).Length;
            }
        }

        /// <summary>
        /// Send a callback to FrmMain to request a GUI update.
        /// </summary>
        /// <param name="statusType"></param>
        /// <param name="additionalMessage">Addition status message</param>
        /// <param name="complete">True if to call the OnEnded method instead of OnUpdate</param>
        /// <param name="ex"></param>
        private void UpdateStatus(StatusType statusType, string additionalMessage, bool complete = false, Exception ex = null)
        {
            this.statusType = statusType;
            string statusName = null;
            switch (statusType)
            {
                case StatusType.Preparing: statusName = "Preparing"; break;
                case StatusType.WaitingForDisc: statusName = "Waiting for disc"; break;
                case StatusType.Copying: statusName = "Copying"; break;
                case StatusType.Done: statusName = "Done"; break;
                case StatusType.Error: statusName = "Error"; break;
                default: statusName = "?"; break;
            }
            this.status = statusName + (additionalMessage == null ? "" : ": " + additionalMessage);

            try
            {
                if (!complete) OnUpdate(this, ex);
                else OnEnded(this);
            }
            catch
            {
                //Ignore any exception, because it would have come from FrmMain
            }
        }

        //[DllImport("winmm.dll", EntryPoint="mciSendStringA", CharSet=CharSet.Ansi)]
        //protected static extern int mciSendString (string mciCommand, StringBuilder returnValue, int returnLength, IntPtr callback);
        [DllImport("winmm.dll", EntryPoint = "mciSendString")]
        public static extern int mciSendStringA(string lpstrCommand, string lpstrReturnString, int uReturnLength, int hwndCallback);

        /// <summary>
        /// Opens the cd tray
        /// </summary>
        /// <param name="driveLetter"></param>
        private static void OpenTray(string driveLetter)
        {
            //int result = mciSendString("set cdaudio door open wait", null, 0, IntPtr.Zero);
            //result = mciSendString("set cdaudio door closed wait", null, 0, IntPtr.Zero);
            string returnString = null;
            mciSendStringA("open " + driveLetter + ": type CDaudio alias drive" + driveLetter, returnString, 0, 0);
            mciSendStringA("set drive" + driveLetter + " door open", returnString, 0, 0);
        }
    }


    /// <summary>
    /// The current status of the DiscCopier process
    /// </summary>
    public enum StatusType {
        Preparing, WaitingForDisc, Copying, Done, Error
    }

    /// <summary>
    /// Used for measuring the average transfer rate
    /// </summary>
    public struct CopySnapshot
    {
        public long BytesCopied, Time;
        public CopySnapshot(long bytesCopied, long time)
        {
            this.BytesCopied = bytesCopied;
            this.Time = time;
        }
    }
}
